
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Collections;
using System.IO;
using System.Reflection;
using System.Xml;

using Swaf;
using Swaf.Call;
using Swaf.Xml;

namespace Swaf.Config
{	
	/// <summary>
	/// Provides support for processing an XML document for application configuration
	/// where registered <c>IConfigHandler</c> objects are used to process the
	/// various contents of the document.</summary>
	public class ConfigManager : IConfigManager
	{
		protected IDictionary absoluteHandlerMap_ = new Hashtable();
		protected IList relativeSuffixes_ = new ArrayList();
		protected IList relativeHandlers_ = new ArrayList();
		protected IApplication m_app;		
		protected IList m_configPartners = new ArrayList();
		protected IDictionary processedConfigFiles_ = new Hashtable();


		internal static bool UseTextReader{get{return false;}}

		public ConfigManager(IApplication app)
		{
			m_app = app;
			registerHandler("<AppConfig>", new AppConfigHandler(this));	
			registerHandler("<Feature>", new FeatureConfigHandler(this));
			registerHandler("...<Import>", new ImportConfigHandler(this));
			registerHandler("...<Dependencies> <Dependency>", new DependencyConfigHandler(this));
			registerHandler("...<ConfigurationClasses> <Class>", new ConfigPartnerConfigHandler(this));
		}
		
		public void  registerHandler(string path, IConfigHandler handler)
		{
			if (path.StartsWith("."))
			{
				// It is a "relative" handler; actual path must END with 'path'
				while (path.StartsWith("."))
				{
					path = path.Substring(1);
				}
				while (path.StartsWith(" "))
				{
					path = path.Substring(1);
				}
				relativeSuffixes_.Add(path);
				relativeHandlers_.Add(handler);
			}
			else
			{
				// It is an "absolute" handler; actual path must be EQUAL to 'path'
				IConfigHandler prev = (IConfigHandler) absoluteHandlerMap_[path];
				// Use the Composite Design Pattern to make a tree of handlers look like a single handler
				if (prev != null)
					handler = new CompositeHandler(prev, handler);
				absoluteHandlerMap_[path] = handler;
			}
		}
		
		public void  registerHandler(string[] paths, IConfigHandler handler)
		{
			 for (int i = 0; i < paths.Length; ++i)
				registerHandler(paths[i], handler);
		}

		public void registerConfigPartner(IConfigPartner partner)
		{
			m_configPartners.Add(partner);
			partner.registerForConfig(this);
		}
		
		public void  readConfigFile(string filename)
		{
			string dir;
			bool isUNC=false;
			try
			{
//#if(!COMPACT_FRAMEWORK)
//                Uri f = new Uri(filename);
//                isUNC=f.IsUnc;
//                dir = f.AbsoluteUri;
//#else
				dir = filename;
//#endif
				int i = dir.LastIndexOfAny(new char[]{'\\','/'}) +1;
				filename = dir.Substring(i);
				dir = dir.Substring(0, i).Replace("///","//");
			}
			catch (UriFormatException e)
			{
				throw new BadConfigException(filename + ": " + e.ToString(), e);
			}

			IDictionary context = new Hashtable();
			context["dir"] = dir;
			context["config"] = this;
			context["app"] = m_app;
			context["isUNC"] = isUNC; 
			readConfigFile(context, filename, true, ConfigManager.UseTextReader,-1);

			foreach(IConfigPartner partner in m_configPartners)
				partner.checkConfigStatus();
		}
		public IDictionary readConfigFile(IDictionary context, string filename, bool isConfig, bool useTextReader, int parentLineNum)
		{
			string dir = ((string)context["dir"]);
			bool isUNC = (bool) context["isUNC"];

			string actualFilename = filename.IndexOf(":") != -1 ? filename : dir + filename;
			string prefix = actualFilename.IndexOf("://") == -1 ? "" : actualFilename.Substring(0,actualFilename.IndexOf("://") + 3).ToLower();
			if (prefix.Equals("file://") || prefix.Equals("filecrypto://"))
			{
				string tmpDir = dir.Substring(prefix.Length).Replace("%20"," ");
				//If this is a UNC file request and the actual file name does not either start with a drive letter or a //, add the //.
				if (isUNC && ! (tmpDir.Substring(1,1).Equals(":") || tmpDir.StartsWith(@"//") || tmpDir.StartsWith(@"\\")) )
				{
					tmpDir = @"//" + tmpDir;
				}

				string tmpName = tmpDir + filename.Replace("%20"," ");
				if(! File.Exists(tmpName))
				{
					foreach(string path in XmlPath.Paths)
					{
						tmpName = tmpDir + path + "\\" + filename;
						if (File.Exists(tmpName))
						{
							actualFilename = prefix + tmpName; 
							break;
						}
					}
				}
				else
				{
					actualFilename = (isUNC?string.Empty:prefix) + tmpName;
				}
			}
			if (actualFilename.EndsWith(".enc") && actualFilename.StartsWith("file://"))
				actualFilename="filecrypto://" + actualFilename.Substring(7);
			if (! processedConfigFiles_.Contains(actualFilename))
			{
				Application.currentApp.Log.println(Application.NORMAL_STARTUP_LOG_LEVEL, "ConfigManager.readFile: file = '{0}'", actualFilename);
				object filenameOld = context["filename"];
				object isConfigOld = context["isConfig"];

				IList fileStack = context["fileStack"] as IList;
				if (fileStack == null)
					context["fileStack"] = fileStack = new ArrayList();
				if (fileStack.Count > 0 && parentLineNum != -1)
					((object[])fileStack[fileStack.Count - 1])[1] = parentLineNum;
				fileStack.Add(new object[] { filename, null });

				IList fileHistory = context["fileHistory"] as IList;
				if (fileHistory == null)
					context["fileHistory"] = fileHistory = new ArrayList();
				fileHistory.Add(filename);

				context["filename"] = filename;
				context["isConfig"] = isConfig.ToString();
				context["_configMgr"] = this;

                processedConfigFiles_[actualFilename]=true;
				try
				{
					if (useTextReader)
					{
						XmlTextReader xmlReader = new XmlTextReader(actualFilename);
						xmlReader.MoveToContent();
						traverse(context, "<" + xmlReader.Name + ">", new XmlTextReaderConfigElement(context, xmlReader));
					}
					else
					{
						LineInfoXmlDocument document = new LineInfoXmlDocument();
						document.PreserveWhitespace = true;
						if (actualFilename.StartsWith("rez://"))
						{
							string rez = actualFilename.Substring(6).Replace('/', '.');
							if(!AssRez.Instance.rezExist(rez))
							//if(ass.GetManifestResourceInfo(rez) == null)
								foreach(string path in XmlPath.Paths)
								{
									rez = dir + path + "." + filename;
									rez = rez.Substring(6).Replace('/', '.');
									if(AssRez.Instance.rezExist(rez))
									//if (ass.GetManifestResourceInfo(rez) != null)
										break;
								}

							using (TextReader r = new StreamReader(AssRez.Instance.getResource(rez)))
							{
								string rezTxt = r.ReadToEnd();
								document.LoadXml(rezTxt);
							}
						}
						else
							document.Load(actualFilename);
						XmlElement root = document.DocumentElement;
						traverse(context, "<" + root.Name + ">", new XmlElementConfigElement(context, root));
					}
				}
				catch (BadConfigException e)
				{
					throw;
				}
				catch (Exception e)
				{
					IApplication app = Application.currentApp;
					if (app.Log.Observers.Count == 0)
					{
						IClassFactory callTypeFactory = app.factories["CallTypes"];
						if (callTypeFactory != null)
						{
							ICall localClass = callTypeFactory.createFromNickName("LocalClass",
								new Swaf.Logging.SimpleLogObserver(), "writeEntry", null) as ICall;
							if (localClass != null)
								app.Log.addObserver(localClass);
						}
					}
					app.Log.println("Exceptions processing file {0}.  Error was {1}", filename, e.ToString());
					string errorInfo = "Config file call chain was:\n";
					fileStack = context["fileStack"] as IList;
					if (fileStack != null)
						foreach (object[] finfo in fileStack)
						{
							errorInfo += String.Format("	{0}", finfo[0]);
							if (finfo[1] != null)
								errorInfo += String.Format(" Line {0}\n", finfo[1]);
							else
								errorInfo += "\n";
						}

					throw new BadConfigException(errorInfo, e);
				}
				finally
				{
					fileStack = context["fileStack"] as IList;
					fileStack.RemoveAt(fileStack.Count - 1);
					context["filename"] = filenameOld;
					context["isConfig"] = isConfigOld;
				}
			}
			else
			{
				//This file has already been processed, skipping.
				Application.currentApp.Log.println(Application.NORMAL_STARTUP_LOG_LEVEL, "ConfigManager.readFile: '{0}' has already been processed.  Skipping.", actualFilename);
			}
			return context;
		}
		
		public virtual void  traverse(IDictionary context, string path, IConfigElement n)
		{
			context["path"] = path;
			// Application.currentApp.Log.println("XML: " + context.get("filename") + ": " + path);
			IList matchingHandlers = new ArrayList();
			IConfigHandler h;
			int num = 0;
			// Get the "absolute" handler
			try
			{
			if ((h = (IConfigHandler) absoluteHandlerMap_[path]) != null)
				matchingHandlers.Add(h);
			// Get all the "relative" handlers
			num = relativeSuffixes_.Count;
			 for (int i = 0; i < num; ++i)
				if (path.EndsWith((string) relativeSuffixes_[i]))
					matchingHandlers.Add(relativeHandlers_[i]);
			// Call the "executeBefore()" methods in left-to-right (forward) order:
			num = matchingHandlers.Count;
			 for (int i = 0; i < num; ++i)
				((IConfigHandler) matchingHandlers[i]).executeBefore(n);

			}
			catch(Exception e)
			{
				IList fileStack = context["fileStack"] as IList;
				if (fileStack != null && fileStack.Count > 0)
					((object[])fileStack[fileStack.Count - 1])[1] = n.LineNumber;
				m_app.Log.println(Application.NORMAL_STARTUP_LOG_LEVEL , 
					"Exception while processing config {0}: Line {1} Pos {2}", context["filename"], n.LineNumber, n.LinePosition);
				throw;
			}
			// Traverse through all the children:
			if(n is XmlElementConfigElement)
			{
				foreach(XmlNode cn in ((XmlElementConfigElement)n).Element.ChildNodes)
					if(cn is XmlElement)
						traverse(context, path + (path.Length == 0?"":" ") + "<" + cn.Name + ">", new XmlElementConfigElement(context, (XmlElement)cn));
			}
			else if(n is XmlTextReaderConfigElement)
			{
				XmlTextReaderConfigElement config = (XmlTextReaderConfigElement)n;
				XmlTextReader xmlReader = config.textReader;
				string s = xmlReader.Name;
				if(!xmlReader.IsEmptyElement && !config.ReadThroughChildren)
				{
					int depth = xmlReader.Depth;
					while(xmlReader.Read())
						if(xmlReader.NodeType == XmlNodeType.Element)
							traverse(context, path + (path.Length == 0?"":" ") + "<" + xmlReader.Name + ">", new XmlTextReaderConfigElement(context, xmlReader));
						else if(xmlReader.NodeType == XmlNodeType.EndElement && xmlReader.Depth == depth)
							break;
				}
			}
			else
				throw new ApplicationException("Unknown implementation of IConfigElement");
			
			// Call the "executeAfter()" methods in right-to-left (reverse) order:
			 for (int i = num - 1; i >= 0; --i)
				((IConfigHandler) matchingHandlers[i]).executeAfter(n);
		}
		
		/// <summary>Uses the <em>Composite Design Pattern</em> to compose <c>IConfigHandler</c> objects.
		/// </summary>
		public class CompositeHandler : ConfigHandler
		{
			public CompositeHandler(IConfigHandler a, IConfigHandler b)
			{
				a_ = a;
				b_ = b;
			}
			public override void executeBefore(IConfigElement configInfo)
			{
				a_.executeBefore(configInfo);
				b_.executeBefore(configInfo);
			}
			public override void executeAfter(IConfigElement configInfo)
			{
				b_.executeAfter(configInfo); // go in reverse order
				a_.executeAfter(configInfo);
			}
			protected internal IConfigHandler a_, b_;
		}
		
		
		private class AppConfigHandler:ConfigHandler
		{
			protected static string[] s_legalAttributes = new string[]{"extends", "name"};
			public AppConfigHandler(ConfigManager enclosingInstance)
			{
				this.enclosingInstance = enclosingInstance;
			}
			public override void executeBefore(IConfigElement configInfo)
			{
				configInfo.checkIllegalAttributes(s_legalAttributes);
				string baseApp = configInfo.getAttribute("extends","");
                Application.globalApp.appData["AppName"] = configInfo.getAttribute("name", "Unknown");
				Application.currentApp.Log.println(Application.NORMAL_STARTUP_LOG_LEVEL,"Read configuration entry for AppConfig: extends={0}",baseApp);
				if(baseApp.Length != 0)
				{
					enclosingInstance.readConfigFile(configInfo.Context, baseApp + ".xml", false, ConfigManager.UseTextReader, configInfo.LineNumber);
				}
			}
			private ConfigManager enclosingInstance;
		}

		private class FeatureConfigHandler:ConfigHandler
		{
			protected static string[] s_legalAttributes = new string[]{"id","name","extends"};
			public FeatureConfigHandler(ConfigManager enclosingInstance)
			{
				this.enclosingInstance = enclosingInstance;
			}
			public override void executeBefore(IConfigElement configInfo)
			{
				configInfo.checkIllegalAttributes(s_legalAttributes);
				string baseApp = configInfo.getAttribute("extends","");
				if(baseApp.Length != 0)
				{
					enclosingInstance.readConfigFile(configInfo.Context, baseApp + ".xml", false, ConfigManager.UseTextReader, configInfo.LineNumber);
				}
				Application.currentApp.Log.println(Application.NORMAL_STARTUP_LOG_LEVEL,"Defined Feature \"{0}\" (Id=\"{1}\") which extends \"{2}\"", configInfo.getAttribute("name",""), configInfo.getAttribute("id",""), baseApp);
			}
			private ConfigManager enclosingInstance;
		}

		private class ImportConfigHandler:ConfigHandler
		{
			protected static string[] s_legalAttributes = new string[]{};
			public ImportConfigHandler(ConfigManager enclosingInstance)
			{
				this.enclosingInstance = enclosingInstance;
			}
			public override void  executeBefore(IConfigElement configInfo)
			{
				configInfo.checkIllegalAttributes(s_legalAttributes);
				enclosingInstance.readConfigFile(configInfo.Context, configInfo.getChildText(), false, ConfigManager.UseTextReader, configInfo.LineNumber);
			}
			private ConfigManager enclosingInstance;
		}

		private class DependencyConfigHandler:ConfigHandler
		{
			protected static string[] s_legalAttributes = new string[]{"type", "stripDepth"};
			private ConfigManager m_cm;

			public DependencyConfigHandler(ConfigManager cm)
			{
				this.m_cm = cm;
			}
			public override void  executeBefore(IConfigElement configInfo)
			{
				configInfo.checkIllegalAttributes(s_legalAttributes);
				string type=configInfo.getAttribute("type", "");
				if(String.Compare(type, "feature") == 0)
				{
					m_cm.readConfigFile(configInfo.Context, configInfo.getChildText(), false, ConfigManager.UseTextReader, configInfo.LineNumber);
				}
			}
		}

		private class ConfigPartnerConfigHandler:ConfigHandler
		{
			protected static string[] s_legalAttributes = new string[]{"name","type"};
			public ConfigPartnerConfigHandler(ConfigManager enclosingInstance)
			{
				this.enclosingInstance = enclosingInstance;
			}
			public override void  executeBefore(IConfigElement configInfo)
			{
				configInfo.checkIllegalAttributes(s_legalAttributes);
				string classType = configInfo.getAttribute("type");
				string name = configInfo.getAttribute("name");
				string errorMessage = string.Empty;
				Exception error = null;
				Application.currentApp.Log.println(Application.VERBOSE_STARTUP_LOG_LEVEL,"Defined Config Partner {0} classType {1}",name, classType);
				try
				{
					IConfigPartner partner = (IConfigPartner)Cmn.callCtor(classType, new Type[]{typeof(IApplication)}, new object[]{Application.currentApp});
					enclosingInstance.registerConfigPartner(partner);
					if(name != null)
						Application.currentApp.appData[name] = partner;
				}
				catch (System.ApplicationException e)
				{
					errorMessage = "Linkage error: " + e.Message;
					error = e;
				}
				catch (Exception e)
				{
					errorMessage = "Exception during initialization: " + e.Message;
					error = e;
				}
				if (error != null)
					xmlError(configInfo.Context, "Unable to load class file " + name + ": " + errorMessage, error);
			}
			private ConfigManager enclosingInstance;
		}
	}
}